[Previous] [Next]

The Basic Concepts

I have noticed that many programmers exposed for the first time to OOP tend to confuse classes and objects, so a very short explanation is in order. A class is a portion of the program (a source code file, in Visual Basic) that defines the properties, methods, and events—in a word, behavior—of one or more objects that will be created during execution. An object is an entity created at run time, which requires memory and possibly other system resources, and is then destroyed when it's no longer needed or when the application ends. In a sense, classes are design time-only entities, while objects are run time-only entities.

Your users will never see a class; rather, they'll probably see and interact with objects created from your classes, such as invoices, customer data, or circles on the screen. As a programmer, your point of view is reversed because the most concrete thing you'll have in front of you while you're writing the application is the class, in the form of a class module in the Visual Basic environment. Until you run the application, an object isn't more real than a variable declared with a Dim statement in a code listing. In my opinion, this dichotomy has prevented many Visual Basic programmers from embracing the OOP paradigm. We have been spoiled by the RAD (Rapid Application Development) orientation of our favorite tool and often think of objects as visible objects, such as forms, controls, and so on. While Visual Basic can also create such visible objects—including Microsoft ActiveX controls—you won't grasp the real power of object orientation until you realize that almost everything in your program can be an object, from concrete and visible entities such as invoices, products, customers, employees, and so on to more abstract ones such as the validation process or the relationship between two tables.

The Main Benefits of OOP

Before getting practical, I'd like to hint at what object-oriented programming has to offer you. I'll do that by listing the key features of OOPLs (object-oriented programming languages) and explaining some concepts. An understanding of these ideas will turn out to be very useful later in the chapter.

Encapsulation

Encapsulation is probably the feature that programmers appreciate most in object-oriented programming. In a nutshell, an object is the sole owner of its own data. All data is stored inside a memory area that can't be directly accessed by another portion of the application, and all assignment and retrieval operations are performed through methods and properties provided by the object itself. This simple concept has at least two far-reaching consequences:

As with most OOP features, it's your responsibility to ensure that the class is well encapsulated. The fact that you're using a class doesn't guarantee that the goals of encapsulation are met. In this and the next chapter, I'll show you how some simple rules—and common sense—help you implement robust classes. A robust class is one that actively protects its internal data from tampering. If an object derived from a class holds valid data and all the operations you perform on that object transform the data only into other valid data (or raise an error if the operation isn't valid), you can be absolutely sure that the object will always be in a valid state and will never propagate a wrong value to the rest of the program. This is a simple but incredibly powerful concept that lets you considerably streamline the process of debugging your code.

The second goal that every programmer should pursue is code reusability, which you achieve by creating classes that are easily maintained and reused in other applications. This is a key factor in reducing the development time and cost. Classes offer much in this respect, but again they require your cooperation. When you start writing a new class, you should always ask yourself: Is there any chance that this class can be useful in other applications? How can I make this class as independent as possible from the particular software I'm developing right now? In most cases, this means adding a few additional properties or additional arguments to methods, but the effort often pays off nicely. Don't forget that you can always resort to default values for properties and optional arguments for methods, so in most cases these enhancements won't really make the code that uses the class more complex than it actually needs to be.

The concept of self-containment is also strictly related to code reuse and encapsulation. If you want to create a class module that's easily reusable, you absolutely must not allow that class to depend on any entity outside it, such as a global variable. This would break encapsulation (because code elsewhere in the application might change the value of the variable to some invalid data) and above all, it would prevent you from reusing the class elsewhere without also copying the global variable (and its parent BAS module). For the same reason, you should try to make the class independent of general-purpose routines located in another module. In most cases, I prefer to duplicate shorter routines in each class module, if this makes the class easily movable elsewhere.

Polymorphism

Informally, Polymorphism is the ability of different classes to expose similar (or identical) interfaces to the outside. The most evident kind of polymorphism in Visual Basic is forms and controls. TextBox and PictureBox controls are completely different objects, but they have some properties and methods in common, such as Left, BackColor, and Move. This similarity simplifies your job as a programmer because you don't have to remember hundreds of different names and syntax formats. More important, it lets you manage a group of controls using a single variable (typed as Control, Variant, or Object) and create generic procedures that act on all the controls on a form and therefore noticeably reduce the amount of code you have to write.

Inheritance

Inheritance is the ability, offered by many OOP languages, to derive a new class (the derived or inherited class) from another class (the base class). The derived class automatically inherits the properties and methods of the base class. For example, you could define a generic Shape class with properties such as Color and Position and then use it as a base for more specific classes (for example, Rectangle, Circle, and so on) that inherit all those generic properties. You could then add specific members, such as Width and Height for the Rectangle class and Radius for the Circle class. It's interesting to note that, while polymorphism tends to reduce the amount of code necessary to use the class, inheritance reduces the code inside the class itself and therefore simplifies the job of the class creator. Unfortunately, Visual Basic doesn't support inheritance, at least not in its more mature form of implementation inheritance. In the next chapter, I show how you can simulate inheritance by manually writing code and explain when and why this can be useful.

Your First Class Module

Creating a class in Visual Basic is straightforward: just issue an Add Class Module command from the Project menu. A new code editor window appears on an empty listing. By default, the first class module is named Class1, so the very first thing you should do is change this into a more appropriate name. In this first example, I show how to encapsulate personal data related to a person, so I'm naming this first class CPerson.

NOTE
I admit it: I'm not a fanatic about naming conventions. Microsoft suggests that you use the cls prefix for class module names, but I don't comply simply because I feel it makes my code less readable. I often prefer to use the shorter C prefix for classes (and I for interfaces), and sometimes I use no prefix at all, especially when objects are grouped in hierarchies. Of course, this is a matter of personal preference, and I don't insist that my system is more rational than any other.

The first version of our class includes only a few properties. These properties are exposed as Public members of the class module itself, as you can see in this code and also in Figure 6-1:

' In the declaration section of the CPerson class module
Public FirstName As String
Public LastName As String

Click to view at full size.

Figure 6-1. Creating a class module, giving it a name in the Properties window, and adding some Public variables in the code editor window.

This is a very simple class, but it's a good starting point for experimenting with some interesting concepts, without being distracted by details. Once you have created a class module, you can declare an object variable that refers to an instance of that class:

' In a form module
Private Sub cmdCreatePerson_Click()
    Dim pers As CPerson                          ' Declare.
    Set pers = New CPerson                       ' Create.
    pers.FirstName = "John"                      ' Assign properties.
    pers.LastName = "Smith"
    Print pers.FirstName & " " & pers.LastName   ' Check that it works.
End Sub

The code's not very impressive, admittedly. But remember that here we're just laying down concepts whose real power will be apparent only when we apply them to more complex objects in real-world applications.

Auto-instancing object variables

Unlike regular variables, which can be used as soon they have been declared, an object variable must be explicitly assigned an object reference before you can invoke the object's properties and methods. In fact, when an object variable hasn't been assigned yet, it contains the special Nothing value: In other words, it doesn't contain any valid reference to an actual object. To see what this means, just try out this code:

Dim pers As CPerson         ' Declare the variable,
' Set pers = New CPerson    ' but comment out the creation step.
Print pers.FirstName        ' This raises an error 91 _ "Object variable
                            ' or With block variable not set"

In most cases, this behavior is desirable because it doesn't make much sense to print a property of an object that doesn't exist. A way to avoid the error is to test the current contents of an object variable using the Is Nothing test:

' Use the variable only if it contains a valid object reference
If Not (pers Is Nothing) Then Print pers.FirstName

In other cases, however, you just want to create an object, any object, and then assign its properties. In these circumstances, you might find it useful to declare an auto-instancing object variable using the As New clause:

Dim pers As New CPerson        ' Auto-instancing variable

When at run time Visual Basic encounters a reference to an auto-instancing variable, it first determines whether it's pointing to an existing object and creates a brand new instance of the class if necessary. Auto-instancing variables have virtues and liabilities, plus a few quirks you should be aware of:

In summary, auto-instancing variables often aren't the best choice, and in general I advise you not to use them. Most of the code shown in this chapter doesn't make use of auto-instancing variables, and you can often do without them in your own applications as well.

Property procedures

Let's go back to the CPerson class and see how the class can protect itself from invalid assignments, such as an empty string for its FirstName or LastName properties. To achieve this goal, you have to change the internal implementation of the class module because in its present form you have no means of trapping the assignment operation. What you have to do is transform those values into Private members and encapsulate them in pairs of Property procedures. This example shows the code for Property Get and Let FirstName procedures, and the code for LastName is similar.

' Private member variables
Private m_FirstName As String
Private m_LastName As String

' Note that all Property procedures are Public by default.
Property Get FirstName() As String
    ' Simply return the current value of the member variable.
    FirstName = m_FirstName
End Property

Property Let FirstName(ByVal newValue As String)
    ' Raise an error if an invalid assignment is attempted.
    If newValue = "" Then Err.Raise 5    ' Invalid procedure argument 
    ' Else store in the Private member variable.
    m_FirstName = newValue
End Property

NOTE
You can save some typing using the Add Procedure command from the Tools menu, which creates for you the templates for Property Get and Let procedures. But you should then edit the result because all properties created in this way are of type Variant.

Add this code and write your own procedures for handling the LastName; then run the program, and you'll see that everything works as before. What you have done, however, is make the class a bit more robust because it now refuses to assign invalid values to its properties. To see what I mean, just try this command:

pers.Name = ""      ' Raises error "Invalid procedure call or argument"

If you trace the program by pressing F8 to advance through individual statements, you'll understand what those two Property procedures actually do. Each time you assign a new value to a property, Visual Basic checks whether there's an associated Property Let procedure and passes it the new value. If your code can't validate the new value, it raises an error and throws the execution back to the caller. Otherwise, the execution proceeds by assigning the value to the private variable m_FirstName. I like to use the m_ prefix to keep the property name and the corresponding private member variable in sync, but this is just another personal preference; feel free to use it or to create your own rules. When the caller code requests the value of the property, Visual Basic executes the corresponding Property Get procedure, which (in this case) simply returns the value of the Private variable. The type expected by the Property Let procedure must match the type of the value returned by the Property Get procedure. In fact, as far as Visual Basic is concerned, the type of the property is the returned type of the Property Get procedure. (This distinction will make more sense later, when I'm explaining Variant properties.)

It isn't always clear what validating a property value really means. Some properties can't be validated without your also considering what happens outside the class. For example, you can't easily validate a product name without accessing a database of products. To keep things simpler, add a new BirthDate property and validate it in a reasonable way:

Private m_BirthDate As Date

Property Get BirthDate() As Date
    BirthDate = m_BirthDate
End Property
Property Let BirthDate(ByVal newValue As Date)
    If newValue >= Now Then Err.Raise 1001, , "Future Birth Date !"
    m_BirthDate = newValue
End Property

Methods

A class module can also include Sub and Function procedures, which are collectively known as methods of the class. As in other types of modules, the only difference between a Function method and a Sub is that a Function method returns a value, whereas a Sub method doesn't. Since Visual Basic lets you invoke a function and discard its return value, I usually prefer to create Function methods that return a secondary value: This practice adds value to the procedure without getting in the way when the user of the class doesn't actually need the return value.

What methods could be useful in this simple CPerson class? When you start dealing with records for many people, you could easily find yourself printing their complete names over and over. So you might want to devise a way to print a full name quickly and simply. The procedural way of thinking that solves this simple task would suggest that you create a function in a global BAS module:

' In a BAS module
Function CompleteName(pers As CPerson) As String
    CompleteName = pers.FirstName & " " & pers.LastName
End Function

While this code works, it isn't the most elegant way to perform the task. In fact, the complete name concept is internal to the class, so you're missing an opportunity to make the class smarter and easier to use. Besides, you're also making it difficult to reuse the class itself because you now have scattered its intelligence all over your application. The best approach is to add a new method to the CPerson class itself:

' In the CPerson class
Function CompleteName() As String
    CompleteName = FirstName & " " & LastName
End Function

' In the form module, you can now execute the method.
Print pers.CompleteName          ' Prints "John Smith"

While you're within the class module, you don't need the dot syntax to refer to the properties of the current instance. On the other hand, if you're within the class and you refer to a Public name for a property (FirstName) instead of the corresponding Private member variable (m_FirstName), Visual Basic executes the Property Get procedure as if the property were referenced from outside the class. This is perfectly normal, and it's even desirable. In fact, you should always try to adhere to the following rule: Reference private member variables in a class only from the corresponding Property Let/Get procedures. If you later modify the internal implementation of the property, you'll have to modify only a small portion of the code in the class module. Sometimes you can't avoid substantial code modifications, but you should do your best to apply this rule as often as you can. Once you understand the mechanism, you can add much intelligence to your class, as in the following code:

Function ReverseName() As String
    ReverseName = LastName & ", " & FirstName
End Function

Remember that you're just adding code and that no additional memory will be used at run time to store the values of complete and reversed names.

The more intelligence you add to your class, the happier the programmer who uses this class (yourself, in most cases) will be. One of the great things about classes is that all the methods and properties you add to them are immediately visible in the Object Browser, together with their complete syntax. If you carefully select the names of your properties and methods, picking the right procedure for each different task becomes almost fun.

The Class Initialize event

As you start building classes, you'll soon notice how often you want to assign a well-defined value to a property at the time of the creation of the object itself, without having to specify it in the caller code. For example, if you're dealing with an Employee object you can reasonably expect that in most cases its Citizenship property is "American" (or whatever nationality applies where you live). Similarly, in most cases the AddressFrom property in a hypothetical Invoice object will probably match the address of the company you're working for. In all cases, you'd like for these default values to be assigned when you create an object, rather than your having to assign them manually in the code that uses the class.

Visual Basic offers a neat way to achieve this goal. In fact, all you have to do is write some statements in the Class_Initialize event of the class module. To have the editor create a template for this event procedure, you select the Class item in the leftmost combo box in the code editor. Visual Basic automatically selects the Initialize item from the rightmost combo box control and inserts the template into the code window. Here's a Citizenship property that defaults to "American":

' The Private member variable
Private m_Citizenship As String

Private Sub Class_Initialize()
    m_Citizenship = "American"
End Sub
' Code for Public Property Get/Let Citizenship procedure ... (omitted)

If you now run the program you have built so far and trace through it, you'll see that as soon as Visual Basic creates the object (the Set command in the form module), the Class_Initialize event fires. The object is returned to the caller with all the properties correctly initialized, and you don't have to assign them in an explicit way. The Class_Initialize event has a matching Class_Terminate event, which fires when the object instance is destroyed by Visual Basic. In this procedure, you usually close your open files and databases and execute your other cleanup tasks. I will describe the Class_Terminate event at the end of this chapter.

Debugging a class module

In most respects, debugging code inside a class module isn't different from debugging code in, say, a form module. But when you have multiple objects that interact with one another, you might easily get lost in the code. Which particular instance are you looking at in a given moment? What are its current properties? Of course, you can use all the usual debugging tools—including Debug.Print statements, data tips, Instant Watch, and so on. But the one that beats them all is the Locals window, which you can see in Figure 6-2. Just keep this window open and you'll know at every moment where you are, how your code affects the object properties, and so on. All in real time.

Click to view at full size.

Figure 6-2. The Locals window is a great debugging tool when you're working with multiple objects.

The Me keyword

Sometimes a class must reference itself in code. This is useful, for instance, when an object must pass a reference to itself to another routine. This can be done using the Me keyword. In the following sample code, I have prepared a couple of general-purpose routines in a BAS module, which help keep track of when an object is created and destroyed:

' In a Standard BAS module
Sub TraceInitialize (obj As Object)
    Debug.Print "Created a " & TypeName(obj) _
        & " object at time " & Time$
End Sub
Sub TraceTerminate (obj As Object)
    Debug.Print "Destroyed a " & TypeName(obj) _
        & " object at time " & Time$
End Sub

Here's how you use these routines from within the CPerson class module:

Private Sub Class_Initialize()
    TraceInitialize Me
End Sub

Private Sub Class_Terminate()
    TraceTerminate Me
End Sub

The Me keyword has other uses as well, as you'll discover in this and the next chapter.